nREPL 0.7.0 で入る Sideloader の話
#Clojure #Advent_Calendar #記事 #nREPL_Sideloader #nrepl
#2019-12-13 #2019-12 #2019
このページは Clojure Advent Calendar 2019 の13日目の穴埋めに向けたものです。
Clojure での開発に CIDER などをお使いの方は nREPL が REPL 駆動開発のベースになっていることをご存知なことが多いと思います。
その nREPL 本体は2019/12/13現在で 0.7.0 のリリースに向けて開発が進められていますが、その中で恐らく目玉になるであろう Sideloader という機能について ちょっと触れたことがあった ので説明してみたいと思います。
実現したいこと
大本の issue はこちらです。
https://github.com/nrepl/nrepl/issues/97
Allow clients to act as network classloaders for the target repl. Thus dependencies can be added on the fly without requiring a shared FS.
要はファイルシステムを共有せずともネットワーク越しの REPL に対して依存ライブラリをその場で(REPLの再起動なしに!)追加できるようになったら嬉しいよね!という感じです。
最終的にこのPRはクローズされ、そこから派生した以下のPRがマージされて、 nrepl 0.7.0-alpha3 にて実際に試せる状態になっています。
https://github.com/nrepl/nrepl/pull/162
挙動
Sideloader が nREPL 上でどのように動くのかを簡単に説明します。
とはいえ以下に公式のドキュメントがあるので、内容はそれとほぼ同じです。
https://nrepl.org/nrepl/building_clients.html
なお nREPL の基本的な動きを知らない場合は nREPLに入門してみた話 という話を以前したので、こちらを先に参照しておくとわかりやすいかもしれません。
まず Sideloader はデフォルトでは有効になっていないので、利用するのは開始するための sideloader-start op を投げる必要があります。
code:json
>> {"session": "xxx", "op": "sideloader-start", "id": 1}
ここで他の operation と違うのは nREPL からのレスポンスはすぐには返ってこないことです。
のちのち必要に応じてこのリクエストを元にしたレスポンスが返ってくるので id を指定しておくと判別が楽です。
その上で以下のようなソースがあり、依存関係として org.clojure/data.json が無いものとします。
code:clojure
(ns foo.core
#_(:require clojure.data.json :as json))
(comment
(json/write-str {:a 1 :b 2}))
ここで ns 内のコメントを外してバッファを評価すると、本来なら Could not locate clojure/data/json__init.class, clojure/data/json.clj or clojure/data/json.cljc on classpath. エラーになるところ、Sideloader が有効だと以下のようなレスポンスが返ってきます。
code:json
>> {"id": 2, "code": "(ns foo.core (:require clojure.data.json :as json))", "session": "xxx", "op": "eval"}
<<< {"id": 1, "status": "sideloader-lookup", "name": "clojure/data/json__init.class", "session": "xxx", "type": "resource"}
clojure/data/json__init.class 頂戴というリクエストを示すレスポンスです。
もし該当のリソースが返せない場合は空文字を content キーに渡します。
code:json
>> {"id": 3, "name": "clojure/data/json__init.class", "session": "xxx", "type": "resource", "op": "sideloader-provide", "content": ""}
<<< {"id": 3, "status": "done", "session": "xxx"}
すると別リソースの要求が返ってきます。先程は *.class ファイルでしたが今度は *.clj ファイルです。
該当のリソースが返せる場合はリソースの内容を Base64 したものを content キーに渡します。
code:json
<<< {"id": 1, "status": "sideloader-lookup", "name": "clojure/data/json.clj", "session": "xxx", "type": "resource"}
>> {"id": 4, "name": "clojure/data/json.clj", "session": "xxx", "type": "resource", "op": "sideloader-provide", "content": "......"}
<<< {"id": 4, "status": "done", "session": "xxx"}
その後、必要なリソースがある限り上記のやりとりが続きます。
そして必要なすべてのリソースが Sideloader 側に渡せると以下のレスポンスが返ってきて、最初の eval op が成功します。
code:json
<<< {"id": 2, "session": "xxx", "value": "nil"}
これで clojure.data.json が解決できたので、そのあとにある json/write-str もエラーなく評価できるようになります。
vim-iced 上で実際に試している動画もあるで、もしよろしければご参照ください。
https://twitter.com/uochan/status/1180627282605461504
課題
実現したいことが実現できることは確認できましたが、特にクライアント側の課題が残っています。
それは「該当のリソースをどう探すか」です。
sideloader-lookup では clojure/data/json__init.class であったり、clojure/data/json.clj といった結構曖昧な情報でリソースを要求してくるので、これをもとに正しいリソースを返すところはクライアント側の努力次第です。
vim-iced では暫定的に ghq のようなリポジトリ管理ツールでソースが管理されているのを前提として、管理ディレクトリのルートから find コマンドなり fd コマンドなりで検索するという無理矢理な方法を現時点では採用していますが、もっと効率的な方法でないと依存が多いライブラリなどを Sideloader 経由で追加する場合に結構時間がかかってしまうのではないかと思います。
そのあたりは自分で使いながら、いろいろと模索していけたらなぁと思います。
最後に
Sideloader 対応はのちのち CIDER にも入るはずなので、待ち遠しい人も多いのではないかと思います。
もし実装されて利用する暁には裏側でこんなやりとりがあるんだなぁというのをしみじみと感じつつクライアント側の努力への感謝を忘れないようにしましょう!